Sie dürfen für die Lösung der Übung zusammenarbeiten, so lange sich ihre Zusammenarbeit auf algorithmische Fragestellungen beschränkt. Bei kopiertem Code oder Text (von Mitstudierenden oder dem Internet) werden alle Lösungen der beteiligten Parteien mit 0 Punkten bewertet. Dazu werden alle Lösungen manuell und automatisiert auf Kopien untersucht.
Einzureichen (online auf mlhub submitten) bis Dienstag, 5. Juni 2018, 24:00 Uhr
Ziel dieser Übung ist das Clustering von Immobilien mit Interpretation der entstandenen Cluster und die Dimensions-Reduktion von Features.
Wir verwenden dazu wiederum den vollen Datensatz /data/house_data.csv der Übung 1.
Vorgaben für die Abgabe :
scikit-learn ist bloss zur Verifikation des Resultats gestattet.#imports
import numpy as np
import pandas as pd
import math
import sys
import random
import matplotlib
from matplotlib import pyplot as plt
from matplotlib import cm
from matplotlib.colors import ListedColormap
from mpl_toolkits.mplot3d import Axes3D
%matplotlib inline
from sklearn.preprocessing import OneHotEncoder, StandardScaler, PolynomialFeatures
from scipy import sparse
from scipy.spatial.distance import cdist
import seaborn as sns
sns.set_style('whitegrid')
_ = np.seterr(all='raise') # to force errors in np to be thrown
_ = np.seterr(all='ignore') # to force errors in np to be thrown
def Abgabe_modus():
'''Funktion um langsame plots auszuschalten während der Entwicklung'''
return True
datapath = '/data/house_data.csv'
df = pd.read_csv(datapath)
print(df.columns)
Clustern Sie die Immobilien indem Sie geeignete Features dafür suchen und allenfalls sinnvoll normalisieren. Erstellen Sie einen Elbow-Plot für $2 \leq k \leq 20$ und wählen Sie eine geeignete Cluster-Anzahl $k$. Führen Sie ein Clustering durch für die gewählte Cluster-Anzahl $k$, visualisieren Sie die entstandenen Cluster mit geeigneten Darstellungen und versuchen Sie, die Cluster zu charakterisieren (welche Eigenschaften haben die Gebäude im jeweiligen Cluster ?)
Eine Möglichkeit, einen Cluster zu charakterisieren, besteht darin, dass Sie untersuchen, welche Features die kleinste Varianz haben (sich am ähnlichsten sind).
# Implementation Algorithm k-Means
class KMeans(object):
def __init__(self, k=3, silent=False):
''' k-Means algorithms '''
self.k = k
self.silent = silent
if (not self.silent):
print('initializing k-means algorithm with {k} k'.format(k=k))
def fit(self, X, use_kpp=True, number_of_initialisations=1000):
best_score = sys.maxsize
for n_init in range(0, number_of_initialisations):
if (use_kpp):
self.initialize_kpp(X)
else:
self.initialize_model(X)
do_step = True
while do_step:
do_step = self.one_iteration(X)
self.num_its_ += 1
cf = self.cost_function(X)
if (cf < best_score):
best_centroids = self.centroids_.copy()
best_labels = self.labels_.copy()
best_cost_ = self.cost_.copy()
best_n_its = self.num_its_
best_score = cf
self.centroids_ = best_centroids.copy()
self.labels_ = best_labels.copy()
self.cost_ = best_cost_.copy()
self.num_its_ = best_n_its
if (not self.silent):
print('finishing with cf', best_score)
print('finishing with ITER COUNT', self.num_its_)
return self
def initialize_model(self, X):
# initialize centroids
self.centroids_ = X[np.random.randint(X.shape[0], size=self.k)]
# assign labels first time
self.labels_ = np.argmin(cdist(X, self.centroids_, metric="sqeuclidean"), axis=1)
#print(self.labels_)
#print(self.centroids_.shape)
# we want to measure the cost function
self.cost_ = []
self.cost_.append(self.cost_function(X))
self.num_its_ = 0
def initialize_kpp(self, X):
'''K-Means++ implementation'''
startIdx= np.random.randint(X.shape[0], size=1)
self.centroids_ = X[startIdx]
copyX = X.copy()
copyX = np.delete(copyX, [startIdx], axis=0)
X_cdist = 0
while (self.centroids_.shape[0] < self.k):
D2 = np.amin(cdist(copyX, self.centroids_, metric='sqeuclidean'), axis=1)
X2_sum = D2.sum()
cum_probabilities = (D2/X2_sum)
idx = np.random.choice(copyX.shape[0], 1, replace=False, p=cum_probabilities)[0]
#r = random.random()
#idx = np.where(cum_probabilities >=r)[0]
self.centroids_ = np.vstack((self.centroids_, copyX[idx]))
copyX = np.delete(copyX, [idx], axis=0)
self.labels_ = np.argmin(cdist(X, self.centroids_, metric='sqeuclidean'), axis=1)
#print(self.labels_)
#print(self.centroids_.shape)
# we want to measure the cost function
self.cost_ = []
self.cost_.append(self.cost_function(X))
self.num_its_ = 0
def one_iteration(self, X):
''' One KMeans iteration
ATTENTION : kicks centroids out if no points are assigned
returns False if centroids are unchanged, True otherwise
'''
old_centroids = self.centroids_.copy()
# update centroids
self.centroids_ = np.array([X[self.labels_ == k].mean(axis=0) for k in range(self.labels_.max()+1)])
self.cost_.append(self.cost_function(X))
# update labels
self.labels_ = np.argmin(cdist(X, self.centroids_, metric="sqeuclidean"), axis=1)
self.cost_.append(self.cost_function(X))
return not np.array_equal(old_centroids, self.centroids_)
def cost_function(self, X):
'''The KMeans cost function.'''
cost_k = []
for ki in range(self.labels_.max()+1):
Wk = cdist(X[self.labels_ == ki], np.atleast_2d(self.centroids_[ki]), metric="sqeuclidean").sum()
cost_k.append(Wk)
return np.array(cost_k).sum()
def plot_kmeans(self, X, ax=None):
KMeans.plot_kmeans_static(X=X, labels=self.labels_, centroids=self.centroids_, ax=ax)
@staticmethod
def plot_kmeans_static(X, labels, centroids=[], ax=None):
colors = sns.color_palette("cubehelix", len(centroids))
if ax is None:
fig, ax = plt.subplots(figsize=(8,8))
for idx, centroid in enumerate(centroids):
ax.scatter(X[labels == idx, 0], X[labels == idx, 1], c=colors[idx])
#ax.plot([centroid[0],],[centroid[1],], '+', color=colors[idx], mew=7, ms=25)
def plot_kmeans_3d(self, X, ax=None):
colors = sns.color_palette("cubehelix", len(self.centroids_))
if (ax == None):
fig = plt.figure()
ax = Axes3D(fig)
for idx, centroid in enumerate(self.centroids_):
ax.scatter(X[self.labels_ == idx, 0], X[self.labels_ == idx, 1],X[self.labels_ == idx,2], c=colors[idx])
ax.set_xlabel('component 1')
ax.set_ylabel('component 2')
ax.set_zlabel('component 3')
return fig, ax
def plot_feature_value_clusters(self, X, features):
fig, ax = plt.subplots(figsize=(20,20))
centroids = self.centroids_
colors = sns.color_palette("Set1", len(centroids)*2)
for idx, centroid in enumerate(centroids):
means = X[self.labels_==idx].mean(axis=0)
ax.plot(range(0,len(features)), means, color=colors[idx], label='mean_{c}'.format(c=idx))
# ax.plot(range(0,len(features)), centroid, color=colors[idx+len(centroids)], label='centroid_{c}'.format(c=idx))
ax.set_ylabel('Durchschnittlicher Wert')
_ = plt.xticks(range(0,len(features)),features)
_ = plt.xticks(rotation=90)
plt.legend()
return fig,ax
def plot_feature_value_clusters_std(self, X, features):
X_display = StandardScaler().fit_transform(X)
return self.plot_feature_value_clusters(X_display, features)
# test ob und wie k++ funktioniert:
def test_kpp():
X = StandardScaler().fit_transform(np.array(df[['price', 'sqft_lot']]))
cf_kpp = []
cf_normal = []
for i in range(0,20):
km = KMeans(k=6, silent=True)
km.fit(X, use_kpp=True, number_of_initialisations=5)
cf_kpp.append(km.cost_function(X))
km_normal = KMeans(k=6, silent=True)
km_normal.fit(X, use_kpp=False, number_of_initialisations=10)
cf_normal.append(km_normal.cost_function(X))
plt.subplots(figsize=(8,5))
plt.plot(cf_kpp, label="K-means++")
plt.plot(cf_normal, label="K-means")
plt.ylabel(r'J')
plt.xlabel('Test #')
plt.xticks(range(0,20))
plt.title('Vergleich K-means++ (5 inits), K-means (10 inits)')
_ = plt.legend()
test_kpp()
K-Means scheint mit 5 Initialisierungen mindestens so gut zu laufen wie 10 zufällige Initialisierungen. Ich entscheide mich deshalb im weiteren Verlaufe K-means++ zu verwenden
if (Abgabe_modus()):
p = sns.pairplot(df)
_= p.fig.suptitle('pairplot')
Auf dem Pairplot sieht man alle Features auf der X-Achse gegenüber allen Features auf der Y-Achse dargestellt. Man sieht hier ein paar Features, welche stark miteinander korrelieren, sqft_above und sqft_living zeigen eine fast perfekte lineare Abhängigkeit. Wie bereits in der ersten bewerteten Übung gesehen, hangen sqft_living und log(price) ebenfalls stark zusammen. Wahrscheinlich hängen sqft_basement und sqft_above logarithmisch oder linear zusammen, das muss man herausfinden. sqft_above und bathrooms scheinen auch zu korrelieren.
Da k-means die least squared distance für die Optimierung verwendet, sind binäre Features (z.B. Waterfront) oder nicht kontinuierliche kategorische Features (z.B. zip) nicht sehr gut geeignet. (Der Centroid würde bei 4045.6 zuliegen kommen, was nicht eindeutig einer Postleizahl zugeordnet werden könnte.) Ich werde als nächstes von allen kontinuierlichen Features die Korrelation visualisieren und aufgrund dessen meine Features auswählen.
# Application
def km_elbow(X, lower_bound=2, upper_bound=20, title='elbow plot', use_kpp = True, n_inits = 10):
cfs_X = []
for k in range(lower_bound, upper_bound + 1):
km = KMeans(k=k, silent=True)
km.fit(X, use_kpp=use_kpp, number_of_initialisations=n_inits)
cfs_X.append(km.cost_function(X))
fig, ax = plt.subplots(figsize=(8,8))
ax.plot(range(2,21),cfs_X)
_ = ax.set_xticks(range(lower_bound,upper_bound + 1))
_ = ax.set_title(title)
_ = ax.set_xlabel('k')
_ = ax.set_ylabel('J')
return fig,ax
all_features = ['price', 'bedrooms', 'bathrooms', 'sqft_living',
'sqft_lot', 'floors', 'waterfront', 'view', 'condition', 'grade',
'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'zipcode',
'lat', 'long', 'sqft_living15', 'sqft_lot15']
cont_features = ['price', 'bedrooms', 'bathrooms', 'sqft_living','sqft_lot', 'floors', 'view', 'condition', 'grade',
'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']
X = np.array(df[cont_features])
X_all = np.array(df[all_features])
xScaler = StandardScaler()
X_std = xScaler.fit_transform(X)
#korrelation-matrix:
sigma = X_std.T.dot(X_std)/X_std.shape[0]
_ = plt.subplots(figsize=(10,10))
_ = plt.imshow(sigma, cmap="hot")
_ = plt.xticks(range(0,len(cont_features)),cont_features)
_ = plt.xticks(rotation=90)
_ = plt.yticks(range(0,len(cont_features)),cont_features)
_ = plt.title('Korrelations-Matrix der Features')
print('sum correlation')
for idx,s in enumerate(sigma.sum(axis=1)):
print(cont_features[idx],':',s/len(cont_features))
Im Print sieht man die Summe der korrelationen pro Feature. Hohe Werte bedeuten, dass das Feature mit vielen andern Features korreliert. Diese Darstellung soll mir nachher helfen Featuers zu eliminieren.
Auf der Korrelationsmatrix dieht man auf der X und Y Achse jeweils die Features gegenüber gestellt. Die Werte sind, wie fest die Features miteinander korrelieren. Hohe Werte (hohe Korrelation) werden heller dargestellt, tiefere Werte (wenig Korrelation) rot / dunkler.
Die Diagonale ist (per definition) Weiss -> 100% Korrelation. Interessant ist, dass sqft_living und sqft_above sehr stark miteinander korrelieren. Ich werde daher nur eines der beiden Features verwenden. Aufgrund der Totalen Korrelation werde ich sqft_living nicht weiter verwenden.
Grade und sqft_living & sqft_above korrelieren auch sehr stark. da ich sqft_living bereits ausgeschlossen habe, werde ich als nächstes nochmals die totale Korrelation berechnen um zu entscheiden, welches der beiden Features (grade / sqft_above) ich ausschliesse
cont_features2 = ['price', 'bedrooms', 'bathrooms','sqft_lot', 'floors', 'view', 'condition', 'grade',
'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']
X = np.array(df[cont_features2])
xScaler = StandardScaler()
X_std = xScaler.fit_transform(X)
#korrelation-matrix:
sigma = X_std.T.dot(X_std)/X_std.shape[0]
print('sum correlation')
for idx,s in enumerate(sigma.sum(axis=1)):
print(cont_features2[idx],':',s/len(cont_features2))
Wie vorher erklärt, sieht man hier nochmals die Summe der Korrelationen. Aufgrund dessen, dass grade die höhere Summe hat, werde ich dieses Feature ausschliessen.
Eigentlich korreliert price auch sehr stark mit den anderen Features. Ich werde daher das Modell 2 mal durchrechnen, einmal mit price und einmal ohne.
features = ['price', 'bedrooms', 'bathrooms','sqft_lot', 'floors', 'view', 'condition', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']
features_wo_price = ['bedrooms', 'bathrooms','sqft_lot', 'floors', 'view', 'condition', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated', 'lat', 'long']
X = np.array(df[features])
X_wo_price = np.array(df[features_wo_price])
X_std = StandardScaler().fit_transform(X)
X_std_wo_price = StandardScaler().fit_transform(X_wo_price)
_, _ = km_elbow(X, use_kpp=True, title='elbow plot')
_, _ = km_elbow(X_std, use_kpp=True, title='elbow plot std')
_, _ = km_elbow(X_wo_price, use_kpp=True, title='elbow plot ohne price')
_, _ = km_elbow(X_std_wo_price, use_kpp=True, title='elbow plot ohne price std')
Auf dem Elbowplot sieht man auf der X-Achse die anzahl Cluster, auf der Y-Achse den Cost_function wert (J) Wenn man die Features standardisiert, so scheint es keinen klaren Ellbogen mehr zu geben.
Ich werde mit folgenden k weiter arbeiten.:
def feature_value_clusters(X, k, title):
km = KMeans(k=k)
km.fit(X, use_kpp=True, number_of_initialisations=500)
fig, ax = km.plot_feature_value_clusters_std(X_all, all_features)
fig.set_size_inches(10,10)
ax.set_title(title)
return km, fig, ax
km_normal,_,_ = feature_value_clusters(X, 6, 'Clustering Features')
km_std,_,_ = feature_value_clusters(X_std, 7, 'Clustering Features std')
km_wo_price,_,_ = feature_value_clusters(X_wo_price, 5, 'Clustering Features ohen Preis')
km_std_wo_price,_,_ = feature_value_clusters(X_std_wo_price, 8, 'Clustering Features ohne Preis std')
def print_cluster_size(km, title):
unique, counts = np.unique(km.labels_, return_counts=True)
totalcnt = km.labels_.shape[0]
print(title,':',dict(zip(unique, counts)))
print(title,'%:',dict(zip(unique, np.round(100* counts / totalcnt))))
print_cluster_size(km_normal, 'cluster size normal')
print_cluster_size(km_std, 'cluster size std')
print_cluster_size(km_wo_price, 'cluster size wo price')
print_cluster_size(km_std_wo_price, 'cluster size wo price std')
Die Plots zeigen auf der X-Achse die verschiedenen Features. Auf der Y-Achse sind die durchschnittswerte (standardisiert zur besseren Übersichtlichkeit) pro Cluster dargestellt. Die Cluster werden mit verschiedenen Farben auseinander gehalten.
Man sieht hier, dass das Clustering ohne zu Standardisieren alle Werte recht gut trennt. Also alle hohen Werte sind in einem Cluster, alle tiefen in einem andern. Interessant ist, dass in einem Cluster extrem teure Häuser mit vielen Bädern und Schlafzimmern, grosser Fläche und guter Aussicht aber schlechtem Zustand sind. Die weiteren Cluster scheinen abstufungen zu sein, welche über alle Features gleich sind. Also hoher preis, viele Bäder, viel Platz etc., weniger hoher Preis, weniger Bäder, weniger Platz, etc.,.. Was auffällt, ist, dass etwa 43% in einem und 35 % in einem andern cluster landen. so sind also c.a. 3/4 aller Daten in zwei Cluster aufgeteilt. der 43% Cluster ist der mit den tiefsten Werten, also tiefer Preis, wenige Bäder etc. der 35%-Cluster ist der, der um 0 herum geht, also ziemlich in dem Mittelwert liegt. Der teure cluster macht nur knapp 1% aus.
Hier scheint es etwas wilder zu und her zu gehen, was auffällt sind die Peask bei sqft_lot und yr_renovated und view. Der eine Cluster scheint vorallem Häuser mit viel Platz mit hoher long (also im Osten) zu beinhalten. ein anderer vorallem tiefe Preise und im süd-westen.
Hier ist die Cluster-verteilung ausgeglichener, der höchste anteil hat ein Clsuter mit 21%, was einem Cluster entspricht, der unterdurchschnittlich tiefe Preise, fast keine Bäder, wenigen Stockwerken, früh gebaut und geographisch im nord westen liegt entspricht. Interessant ist, dass es einen Cluster gibt mit extrem hohen Preisen und guter Aussicht, aber ein anderer mit teuren Preisen mit super Aussicht. Der springende Unterschied bei den beiden Clustern ist der Wohnraum welcher bei den extrem teuren Häusern vie lgrösser ist.
Es zeigt sich hier ein ähnliches Bild wie beim Clustering ohne zu standardisieren inkl. Preis. Betrachtet man allerdings die Anteile der cluster etwas genauer, so fallen 92% in einen Cluster! Ich werde daher diesen Ansatz verwerfen und nicht weiter verfolgen. Das Clustering scheint sich ausschliesslich auf sqft_lot zu beziehen. Das war anzunehmen, da es sich hierbei um einen hohen Wert im Gegensatz zu den andern Features handelt.
Hier zeigt sich ein ähnliches Bild wie beim Clustering standardisieren inkl. Preis. Allerdings mit dem Unterschied, dass es wie beim Clustering ohne standardisieren ohne Preis einen Cluster mit extrem viel sqft_lot gibt. Dieser macht knapp 1% aller Häuser aus. Im gegensatz zu dem Clustering standardisieren inkl. Preis gibt es hier einen Cluster mit extrem hohen Preisen und super Aussicht und grossem Wohnraum.
#new dfs with cluster
df_normal = df.assign(cluster=km_normal.labels_)
df_std = df.assign(cluster=km_std.labels_)
df_std_wo_price = df.assign(cluster=km_std_wo_price.labels_)
df_full_std = pd.DataFrame(StandardScaler().fit_transform(X=np.array(df[all_features])),columns=all_features)
print('Varianz in den Cluster')
df_normal_var = df_full_std[features].assign(cluster=km_normal.labels_).groupby('cluster').var()
print('Totale Varianz: ', df_normal_var.sum().sum())
print('Durchscnittliche Varianz pro Cluster / Features: ', df_normal_var.sum().sum() / len(km_normal.centroids_) / len(features))
df_normal_var.style.background_gradient()
#df_normal.groupby('cluster').var()
print('Varianz in den Cluster std')
df_std_var = df_full_std[features].assign(cluster=km_std.labels_).groupby('cluster').var()
print('Totale Varianz: ', df_std_var.sum().sum())
print('Durchscnittliche Varianz pro Cluster: ', df_std_var.sum().sum() / len(km_std.centroids_) / len(features))
df_std_var.style.background_gradient()
print('Varianz in den Cluster ohne Preise std')
df_wo_price_std_var = df_full_std[features_wo_price].assign(cluster=km_std_wo_price.labels_).groupby('cluster').var()
print('Totale Varianz: ', df_wo_price_std_var.sum().sum())
print('Durchscnittliche Varianz pro Cluster: ', df_wo_price_std_var.sum().sum() / len(km_std_wo_price.centroids_) / len(features_wo_price))
df_wo_price_std_var.style.background_gradient()
4 von 5 Cluster zeigen eine geringe Varianz in Preis, Bäder und Kellerplatz. Es gibt einen Cluster, welcher eine sehr Hohe Varianz in den Preisen aufweist. Dies ist der Cluster mit den sehr hohen durchschnittspreisen. Die durchschnittliche Varianz (Summe der Varianz / anzahl Cluster / anzahl Features) liegt bei 1.4.
Hier fällt auf, dass vorallem sqft_lot sehr geringe Varianz hat und dieses Feature zum Clustern verwendet wurde. Die durchscnittliche Varianz pro Cluster liegt bei 0.93
5 von 8 Cluster haben generell eine niedrige Varianz. Zwei Cluster haben eine hohe Varianz in floor, view, condition, sqft_above, sqft_basement und year_built und ein Cluster steht heraus, da er durchgehend hohe Varianz aufweist. Die durchscnittliche Varianz pro Cluster: 0.81
Ein grosser Unterschied zwischen der standardisierten Lösung und der nicht standardisierten ist, dass sqft_lot bei der nicht-standardisierten Lösung eine höhere Varianz aufweist als bei den standardisierten.
def plot_cluster_map(df, title):
fig, ax = plt.subplots(figsize=(6,6))
sc = ax.scatter(df['long'],df['lat'], c=np.array(df['cluster']), cmap=cm.Set1, alpha=0.5)
_ = ax.set_xlabel('long')
_ = ax.set_ylabel('lat')
_ = ax.set_title(title)
plot_cluster_map(df_normal, 'Clusters auf der Karte')
plot_cluster_map(df_std, 'Clusters auf der Karte std')
plot_cluster_map(df_std_wo_price, 'Clusters auf der Karte ohne Preis std')
Man sieht hier auf der X-Achse die long und auf der Y-Achse die lat. Colorcoded jeweils die Cluster. Generell ist zu sehen, dass obowohl lat und long zum Clustering hinzugezogen wurden, die meisten Cluster sehr grossräumig verteilt sind und nicht geographisch stark festhangen.
if (Abgabe_modus()):
p = sns.pairplot(df_normal, hue='cluster', markers=['^','v','<','>','s','o'])
_= p.fig.suptitle('pairplot normal')
p_std = sns.pairplot(df_std, hue='cluster')
_= p_std.fig.suptitle('pairplot std')
p_std_wo_price = sns.pairplot(df_std_wo_price, hue='cluster')
_= p_std_wo_price.fig.suptitle('pairplot ohne Preise std')